/* Mednafen - Multi-system Emulator
 *
 * Copyright notice for this file:
 *  Copyright (C) 2004 Ki
 *  Copyright (C) 2007-2011 Mednafen Team
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
   PCE_FAST(less accurate, faster, etc.) fork from PCE module "pcecd.cpp".
*/

#include <stdlib.h>
#include <string.h>

#include "../mednafen-types.h"
#include "../state.h"
#include "../okiadpcm.h"
#include "../cdrom/SimpleFIFO.h"
#include "../state_helpers.h"

#include "pcecd_drive.h"
#include "pcecd.h"

static unsigned int OC_Multiplier;

static void (*IRQCB)(bool asserted);

static float CDDAVolumeSetting; // User setting!

static bool	bBRAMEnabled;
static uint8	_Port[15];
static uint8 	ACKStatus;

static SimpleFIFO SubChannelFIFO(16);

static Blip_Buffer *sbuf[2];
static int16 RawPCMVolumeCache[2];

static int32 ClearACKDelay;

static int32 lastts;
static int32 pcecd_drive_ne = 0;

typedef Blip_Synth ADSynth;
static ADSynth ADPCMSynth;
static OKIADPCM_Decoder<OKIADPCM_MSM5205> MSM5205;

static bool ADPCMLP;
typedef struct
{
 uint8    *RAM;	// = NULL; //0x10000;
 uint16   Addr;
 uint16   ReadAddr;
 uint16   WriteAddr;
 uint16   LengthCount;

 bool HalfReached;
 bool EndReached;
 bool Playing;

 uint8 LastCmd;
 uint32 SampleFreq;
 uint32 LPF_SampleFreq;

 uint8 PlayBuffer;
 uint8 ReadBuffer;
 int32 ReadPending;
 int32 WritePending;
 uint8 WritePendingValue;

 uint32 PlayNibble;


 int64 bigdivacc;
 int64 bigdiv;
 int32 last_pcm;
} ADPCM_t;

static ADPCM_t ADPCM;

typedef struct
{
 uint8 Command;
 int32 Volume;

 int32 CycleCounter;
 uint32 CountValue;     // What to reload CycleCounter with when it expires.
 bool Clocked;
} FADE_t;

static FADE_t Fader;
static int32 ADPCMFadeVolume, CDDAFadeVolume;

static INLINE void Fader_SyncWhich(void)
{
   if(Fader.Command & 0x2) // ADPCM fade
   {
      ADPCMFadeVolume = Fader.Volume;
      CDDAFadeVolume = 65536;
   }
   else   // CD-DA Fade
   {
      CDDAFadeVolume = Fader.Volume;
      ADPCMFadeVolume = 65536;
   }

   ADPCMFadeVolume >>= 6;
   PCECD_Drive_SetCDDAVolume(0.50f * CDDAFadeVolume * CDDAVolumeSetting);
}

static INLINE int32 ADPCM_ClocksToNextEvent(void)
{
   int32 ret = (ADPCM.bigdiv + 65535) >> 16;

   if(ADPCM.WritePending && ret > ADPCM.WritePending)
      ret = ADPCM.WritePending;

   if(ADPCM.ReadPending && ret > ADPCM.ReadPending)
      ret = ADPCM.ReadPending;

   return(ret);
}

static int32 CalcNextEvent(int32 base)
{
   int32 next_event = base;
   int32 ADPCM_ctne = ADPCM_ClocksToNextEvent();

   if(next_event > ADPCM_ctne)
      next_event = ADPCM_ctne;

   if(ClearACKDelay > 0 && next_event > ClearACKDelay)
      next_event = ClearACKDelay;

   if(next_event > pcecd_drive_ne)
      next_event = pcecd_drive_ne;

   if(Fader.Clocked && next_event > Fader.CycleCounter)
      next_event = Fader.CycleCounter;

   return(next_event);
}

static void update_irq_state()
{
        uint8           irq = _Port[2] & _Port[0x3] & (0x4|0x8|0x10|0x20|0x40);

	IRQCB((bool)irq);
}

static void StuffSubchannel(uint8 meow, int subindex)
{
   uint8 tmp_data = meow & 0x7F;

   if(subindex == -2)
      tmp_data = 0x00;
   else if(subindex == -1)
      tmp_data = 0x80;

   if(SubChannelFIFO.CanWrite())
      SubChannelFIFO.Write(&tmp_data, 1);

   _Port[0x3] |= 0x10;
   update_irq_state();
}

static void CDIRQ(int type)
{
   if(type & 0x8000)
   {
      type &= 0x7FFF;
      if(type == PCECD_Drive_IRQ_DATA_TRANSFER_DONE)
         _Port[0x3] &= ~0x20;
      else if(type == PCECD_Drive_IRQ_DATA_TRANSFER_READY)
         _Port[0x3] &= ~0x40;
   }
   else if(type == PCECD_Drive_IRQ_DATA_TRANSFER_DONE)
   {
      _Port[0x3] |= 0x20;
   }
   else if(type == PCECD_Drive_IRQ_DATA_TRANSFER_READY)
   {
      _Port[0x3] |= 0x40;
   }
   update_irq_state();
}

static void UpdateADPCMIRQState(void)
{
   _Port[0x3] &= ~0xC;

   _Port[0x3] |= ADPCM.HalfReached ? 0x4 : 0x0;	
   _Port[0x3] |= ADPCM.EndReached ? 0x8 : 0x0;

   update_irq_state();
}

static INLINE uint8 read_1808(int32 timestamp)
{
   uint8 ret = PCECD_Drive_GetDB();

   if(PCECD_Drive_GetREQ() && !PCECD_Drive_GetACK() && !PCECD_Drive_GetCD())
   {
      if(PCECD_Drive_GetIO())
      {
         PCECD_Drive_SetACK(true);
         ACKStatus = true;
         pcecd_drive_ne = PCECD_Drive_Run(timestamp);
         ClearACKDelay = 15 * 3;
      }
   }

   return(ret);
}

bool PCECD_SetSettings(const PCECD_Settings *settings)
{
   CDDAVolumeSetting = settings ? settings->CDDA_Volume : 1.0;
   Fader_SyncWhich();

   Blip_Synth_set_volume(&ADPCMSynth, 0.42735f * (settings ? settings->ADPCM_Volume : 1.0), 0x4000);
   ADPCMLP = settings ? settings->ADPCM_LPF : 0;

   PCECD_Drive_SetTransferRate(126000 * (settings ? settings->CD_Speed : 1));

   return true;
}

bool PCECD_Init(const PCECD_Settings *settings, void (*irqcb)(bool), double master_clock, unsigned int ocm, Blip_Buffer *soundbuf_l, Blip_Buffer *soundbuf_r)
{
   lastts = 0;

   OC_Multiplier = ocm;

   IRQCB = irqcb;

   sbuf[0] = soundbuf_l;
   sbuf[1] = soundbuf_r;

   // Warning: magic number 126000 in PCECD_SetSettings() too
   PCECD_Drive_Init(3 * OC_Multiplier, sbuf[0], sbuf[1], 126000 * (settings ? settings->CD_Speed : 1), master_clock * OC_Multiplier, CDIRQ, StuffSubchannel);

   if(!(ADPCM.RAM = (uint8 *)malloc(0x10000)))
   {
      return(0);
   }

   PCECD_SetSettings(settings);

   ADPCM.bigdivacc = (int64)((double)master_clock * OC_Multiplier * 65536 / 32087.5);

   return true;
}


void PCECD_Close(void)
{
   if(ADPCM.RAM)
      free(ADPCM.RAM);
   ADPCM.RAM = NULL;
   PCECD_Drive_Close();
}


void PCECD_Power(uint32 timestamp)
{
   if((int32)timestamp != lastts)
      (void)PCECD_Run(timestamp);

   IRQCB(0);

   PCECD_Drive_Power(timestamp);
   pcecd_drive_ne = 0x7fffffff;

   bBRAMEnabled = false;
   memset(_Port, 0, sizeof(_Port));
   ACKStatus = 0;
   ClearACKDelay = 0;

   memset(ADPCM.RAM, 0x00, 65536);

   ADPCM.ReadPending = ADPCM.WritePending = 0;
   ADPCM.ReadBuffer = 0;
   ADPCM.PlayBuffer = 0;

   ADPCM.LastCmd = 0;
   MSM5205.SetSample(0x800);
   MSM5205.SetSSI(0);

   ADPCM.SampleFreq = 0;
   ADPCM.LPF_SampleFreq = 0;
   ADPCM.bigdiv = ADPCM.bigdivacc * (16 - ADPCM.SampleFreq);

   ADPCM.Addr = 0;
   ADPCM.ReadAddr = 0;
   ADPCM.WriteAddr = 0;
   ADPCM.LengthCount = 0;
   ADPCM.LastCmd = 0;

   ADPCM.HalfReached = false;
   ADPCM.EndReached = false;
   ADPCM.Playing = false;
   ADPCM.PlayNibble = 0;

   UpdateADPCMIRQState();

   Fader.Command = 0x00;
   Fader.Volume = 0;
   Fader.CycleCounter = 0;
   Fader.CountValue = 0;
   Fader.Clocked = false;
}

bool PCECD_IsBRAMEnabled(void)
{
   return bBRAMEnabled;
}

uint8 PCECD_Read(uint32 timestamp, uint32 A)
{
   uint8 ret = 0;

   if((A & 0x18c0) == 0x18c0)
   {
      switch (A & 0x18cf)
      {
         case 0x18c1: ret = 0xaa; break;
         case 0x18c2:	ret = 0x55; break;
         case 0x18c3: ret = 0x00; break;
         case 0x18c5:	ret = 0xaa; break;
         case 0x18c6: ret = 0x55; break;
         case 0x18c7:	ret = 0x03; break;
      }
   }
   else
   {
      PCECD_Run(timestamp);

      switch(A & 0xf)
      {
         case 0x0:
            ret = 0;
            ret |= PCECD_Drive_GetBSY() ? 0x80 : 0x00;
            ret |= PCECD_Drive_GetREQ() ? 0x40 : 0x00;
            ret |= PCECD_Drive_GetMSG() ? 0x20 : 0x00;
            ret |= PCECD_Drive_GetCD() ? 0x10 : 0x00;
            ret |= PCECD_Drive_GetIO() ? 0x08 : 0x00;
            break;

         case 0x1: ret = PCECD_Drive_GetDB();
                   break;

         case 0x2: ret = _Port[2];
                   break;

         case 0x3: bBRAMEnabled = false;

                   /* switch left/right of digitized cd playback */
                   ret = _Port[0x3];
                   _Port[0x3] ^= 2;
                   break;

         case 0x4: ret = _Port[4];
                   break;

         case 0x5: if(_Port[0x3] & 0x2)
                      ret = RawPCMVolumeCache[1] & 0xff;	// Right
                   else
                      ret = RawPCMVolumeCache[0] & 0xff;	// Left
                   break;

         case 0x6: if(_Port[0x3] & 0x2)
                      ret = ((uint16)RawPCMVolumeCache[1]) >> 8;	// Right
                   else
                      ret = ((uint16)RawPCMVolumeCache[0]) >> 8;	// Left
                   break;

         case 0x7:
                   if(SubChannelFIFO.in_count > 0)
                      ret = SubChannelFIFO.ReadUnit();
                   else
                      ret = 0x00;	// Not sure if it's 0, 0xFF, the last byte read, or something else.

                   if(SubChannelFIFO.in_count == 0)
                   {
                      _Port[0x3] &= ~0x10;
                      update_irq_state();
                   }
                   break;

         case 0x8:
                   ret = read_1808(timestamp);
                   break;

         case 0xa: 
                   ADPCM.ReadPending = 19 * 3; //24 * 3;
                   ret = ADPCM.ReadBuffer;
                   break;

         case 0xb: 
                   ret = _Port[0xb];
                   break;

         case 0xc:
                   ret = 0x00;

                   ret |= (ADPCM.EndReached) ? 0x01 : 0x00;
                   ret |= (ADPCM.Playing) ? 0x08 : 0x00;
                   ret |= (ADPCM.WritePending > 0) ? 0x04 : 0x00;
                   ret |= (ADPCM.ReadPending > 0) ? 0x80 : 0x00;
                   break;   

         case 0xd: 
                   ret = ADPCM.LastCmd;
                   break;
      }
   }

   return(ret);
}

static INLINE void Fader_Run(const int32 clocks)
{
   if(Fader.Clocked)
   {
      Fader.CycleCounter -= clocks;
      while(Fader.CycleCounter <= 0)
      {
         if(Fader.Volume)
            Fader.Volume--;

         Fader_SyncWhich();

         Fader.CycleCounter += Fader.CountValue;
      }
   }
}


void PCECD_Write(uint32 timestamp, uint32 physAddr, uint8 data)
{
   const uint8 V = data;

   PCECD_Run(timestamp);

   switch (physAddr & 0xf)
   {
      case 0x0:
         PCECD_Drive_SetSEL(1);
         PCECD_Drive_Run(timestamp);
         PCECD_Drive_SetSEL(0);
         pcecd_drive_ne = PCECD_Drive_Run(timestamp);

         /* reset irq status */
         _Port[0x3] &= ~(0x20 | 0x40);	// TODO: Confirm writing this register really reset these bits.
         update_irq_state();
         break;

      case 0x1:		// $1801
         _Port[1] = data;
         PCECD_Drive_SetDB(data);
         pcecd_drive_ne = PCECD_Drive_Run(timestamp);
         break;

      case 0x2:		// $1802

         PCECD_Drive_SetACK(data & 0x80);
         pcecd_drive_ne = PCECD_Drive_Run(timestamp);
         _Port[2] = data;
         ACKStatus = (bool)(data & 0x80);
         update_irq_state();
         break;

      case 0x3:		// read only
         break;

      case 0x4:
         PCECD_Drive_SetRST(data & 0x2);
         pcecd_drive_ne = PCECD_Drive_Run(timestamp);
         if(data & 0x2)
         {
            _Port[0x3] &= ~0x70;
            update_irq_state();
         }
         _Port[4] = data;
         break;

      case 0x5:
      case 0x6:
         {
            int16 left, right;
            PCECD_Drive_GetCDDAValues(&left, &right);
            RawPCMVolumeCache[0] = ((int64)abs(left) * CDDAFadeVolume) >> 16;
            RawPCMVolumeCache[1] = ((int64)abs(right) * CDDAFadeVolume) >> 16;
         }
         break;

      case 0x7:	// $1807: D7=1 enables backup ram 
         if (data & 0x80)
            bBRAMEnabled = true;
         break;

      case 0x8:	// Set ADPCM address low
         if(ADPCM.LastCmd & 0x80)
            break;

         ADPCM.Addr &= 0xFF00;
         ADPCM.Addr |= V;

         // Length appears to be constantly latched when D4 is set(tested on a real system)
         if(ADPCM.LastCmd & 0x10)
            ADPCM.LengthCount = ADPCM.Addr;
         break;

      case 0x9:	// Set ADPCM address high
         if(ADPCM.LastCmd & 0x80)
            break;

         ADPCM.Addr &= 0x00FF;
         ADPCM.Addr |= V << 8;

         // Length appears to be constantly latched when D4 is set(tested on a real system)
         if(ADPCM.LastCmd & 0x10)
            ADPCM.LengthCount = ADPCM.Addr;
         break;

      case 0xa:
         ADPCM.WritePending = 3 * 11;
         ADPCM.WritePendingValue = data;
         break;

      case 0xb:	// adpcm dma
         _Port[0xb] = data;
         break;

      case 0xc:		// read-only
         break;

      case 0xd:
         if(data & 0x80)
         {
            ADPCM.Addr = 0;
            ADPCM.ReadAddr = 0;
            ADPCM.WriteAddr = 0;
            ADPCM.LengthCount = 0;
            ADPCM.LastCmd = 0;

            ADPCM.Playing = false;
            ADPCM.HalfReached = false;
            ADPCM.EndReached = false;

            ADPCM.PlayNibble = 0;

            UpdateADPCMIRQState();

            MSM5205.SetSample(0x800);
            MSM5205.SetSSI(0);
            break;
         }

         if(ADPCM.Playing && !(data & 0x20))
            ADPCM.Playing = false;

         if(!ADPCM.Playing && (data & 0x20))
         {
            ADPCM.bigdiv = ADPCM.bigdivacc * (16 - ADPCM.SampleFreq);
            ADPCM.Playing = true;
            ADPCM.HalfReached = false;	// Not sure about this.
            ADPCM.PlayNibble = 0;
            MSM5205.SetSample(0x800);
            MSM5205.SetSSI(0);
         }

         // Length appears to be constantly latched when D4 is set(tested on a real system)
         if(data & 0x10)
         {
            ADPCM.LengthCount = ADPCM.Addr;
            ADPCM.EndReached = false;
         }

         // D2 and D3 control read address
         if(!(ADPCM.LastCmd & 0x8) && (data & 0x08))
         {
            if(data & 0x4)
               ADPCM.ReadAddr = ADPCM.Addr;
            else
               ADPCM.ReadAddr = (ADPCM.Addr - 1) & 0xFFFF;

         }

         // D0 and D1 control write address
         if(!(ADPCM.LastCmd & 0x2) && (data & 0x2))
         {
            ADPCM.WriteAddr = ADPCM.Addr;
            if(!(data & 0x1))
               ADPCM.WriteAddr = (ADPCM.WriteAddr - 1) & 0xFFFF;
         }
         ADPCM.LastCmd = data;
         UpdateADPCMIRQState();
         break;

      case 0xe:		// Set ADPCM playback rate
         {
            uint8 freq = V & 0x0F;
            ADPCM.SampleFreq = freq;
         }
         break;

      case 0xf:
         Fader.Command = V;

         // Cancel fade
         if(!(V & 0x8))
         {
            Fader.Volume = 65536;
            Fader.CycleCounter = 0;
            Fader.CountValue = 0;
            Fader.Clocked = false;
         }
         else
         {
            Fader.CountValue = OC_Multiplier * 3 * ((V & 0x4) ? 273 : 655);	// 2.500s : 6.000s;

            if(!Fader.Clocked)
               Fader.CycleCounter = Fader.CountValue;

            Fader.Clocked = true;
         }
         Fader_SyncWhich();
         break;
   }
}

static INLINE void ADPCM_PB_Run(int32 basetime, int32 run_time)
{
   ADPCM.bigdiv -= run_time * 65536;

   while(ADPCM.bigdiv <= 0)
   {
      ADPCM.bigdiv += ADPCM.bigdivacc * (16 - ADPCM.SampleFreq);

      if(ADPCM.Playing && !ADPCM.PlayNibble)	// Do playback sample buffer fetch.
      {
         ADPCM.HalfReached = (ADPCM.LengthCount < 32768);
         if(!ADPCM.LengthCount && !(ADPCM.LastCmd & 0x10))
         {
            if(ADPCM.EndReached)
               ADPCM.HalfReached = false;

            ADPCM.EndReached = true;

            if(ADPCM.LastCmd & 0x40)
               ADPCM.Playing = false;
         }

         ADPCM.PlayBuffer = ADPCM.RAM[ADPCM.ReadAddr];
         ADPCM.ReadAddr = (ADPCM.ReadAddr + 1) & 0xFFFF;

         if(ADPCM.LengthCount && !(ADPCM.LastCmd & 0x10))
            ADPCM.LengthCount--;
      }

      if(ADPCM.Playing)
      {
         int32 pcm;
         uint8 nibble;

         nibble = (ADPCM.PlayBuffer >> (ADPCM.PlayNibble ^ 4)) & 0x0F;
         pcm = MSM5205.Decode(nibble) - 2048;

         ADPCM.PlayNibble ^= 4;

         pcm = (pcm * ADPCMFadeVolume) >> 8;
         uint32 synthtime = ((basetime + (ADPCM.bigdiv >> 16))) / (3 * OC_Multiplier);

         if(sbuf[0] && sbuf[1])
         {
            Blip_Synth_offset(&ADPCMSynth, synthtime, pcm - ADPCM.last_pcm, sbuf[0]);
            Blip_Synth_offset(&ADPCMSynth, synthtime, pcm - ADPCM.last_pcm, sbuf[1]);
         }
         ADPCM.last_pcm = pcm;
      }
   }
}

static INLINE void ADPCM_Run(const int32 clocks, const int32 timestamp)
{
   ADPCM_PB_Run(timestamp, clocks);

   if(ADPCM.WritePending)
   {
      ADPCM.WritePending -= clocks;
      if(ADPCM.WritePending <= 0)
      {
         ADPCM.HalfReached = (ADPCM.LengthCount < 32768);
         if(!(ADPCM.LastCmd & 0x10) && ADPCM.LengthCount < 0xFFFF)
            ADPCM.LengthCount++;

         ADPCM.RAM[ADPCM.WriteAddr++] = ADPCM.WritePendingValue;
         ADPCM.WritePending = 0;
      }
   }

   if(!ADPCM.WritePending)
   {
      if(_Port[0xb] & 0x3)
      {
         // Run PCECD_Drive before we examine the signals.
         pcecd_drive_ne = PCECD_Drive_Run(timestamp);

         if(!PCECD_Drive_GetCD() && PCECD_Drive_GetIO() && PCECD_Drive_GetREQ() && !PCECD_Drive_GetACK())
         {
            ADPCM.WritePendingValue = read_1808(timestamp);
            ADPCM.WritePending = 10 * 3;
         }
      }
   }

   if(ADPCM.ReadPending)
   {
      ADPCM.ReadPending -= clocks;
      if(ADPCM.ReadPending <= 0)
      {
         ADPCM.ReadBuffer = ADPCM.RAM[ADPCM.ReadAddr];
         ADPCM.ReadAddr = (ADPCM.ReadAddr + 1) & 0xFFFF;
         ADPCM.ReadPending = 0;

         ADPCM.HalfReached = (ADPCM.LengthCount < 32768);
         if(!(ADPCM.LastCmd & 0x10))
         {
            if(ADPCM.LengthCount)
               ADPCM.LengthCount--;
            else
            {
               ADPCM.EndReached = true;
               ADPCM.HalfReached = false;

               if(ADPCM.LastCmd & 0x40)
                  ADPCM.Playing = false;
            }
         }
      }
   }

   UpdateADPCMIRQState();
}

extern "C" void PCECD_Run(uint32 in_timestamp)
{
   int32 clocks = in_timestamp - lastts;
   int32 running_ts = lastts;

   while(clocks > 0)
   {
      int32 chunk_clocks = CalcNextEvent(clocks);

      running_ts += chunk_clocks;

      if(ClearACKDelay > 0)
      {
         ClearACKDelay -= chunk_clocks;
         if(ClearACKDelay <= 0)
         {
            ACKStatus = false;
            PCECD_Drive_SetACK(false);
            PCECD_Drive_Run(running_ts);
            if(PCECD_Drive_GetCD())
            {
               _Port[0xb] &= ~1;
            }
         }
      }

      Fader_Run(chunk_clocks);

      ADPCM_Run(chunk_clocks, running_ts);
      pcecd_drive_ne = PCECD_Drive_Run(running_ts);

      clocks -= chunk_clocks;
   }

   lastts = in_timestamp;
}

void PCECD_ResetTS(void)
{
   PCECD_Drive_ResetTS();
   lastts = 0;
}

static int ADPCM_StateAction(StateMem *sm, int load, int data_only)
{
   uint32 ad_sample = MSM5205.GetSample();
   int32  ad_ref_index = MSM5205.GetSSI();

   SFORMAT StateRegs[] =
   {
      SFARRAY(ADPCM.RAM, 0x10000),

      SFVAR(ADPCM.bigdiv),
      SFVAR(ADPCM.Addr),
      SFVAR(ADPCM.ReadAddr),
      SFVAR(ADPCM.WriteAddr),
      SFVAR(ADPCM.LengthCount),
      SFVAR(ADPCM.LastCmd),
      SFVAR(ADPCM.SampleFreq),

      SFVAR(ADPCM.ReadPending),
      SFVAR(ADPCM.ReadBuffer),
      SFVAR(ADPCM.PlayBuffer),

      SFVAR(ADPCM.WritePending),
      SFVAR(ADPCM.WritePendingValue),

      SFVAR_BOOL(ADPCM.HalfReached),
      SFVAR_BOOL(ADPCM.EndReached),
      SFVAR_BOOL(ADPCM.Playing),

      SFVAR(ADPCM.PlayNibble),

      SFVAR(ad_sample),
      SFVAR(ad_ref_index),
      SFEND
   };

   int ret = MDFNSS_StateAction(sm, load, data_only, StateRegs, "APCM", false);
   if(load)
   {
      MSM5205.SetSample(ad_sample);
      MSM5205.SetSSI(ad_ref_index);
   }
   return(ret);
}

int PCECD_StateAction(StateMem *sm, int load, int data_only)
{
   SFORMAT StateRegs[] =
   {
      SFVAR_BOOL(bBRAMEnabled),
      SFVAR(ACKStatus),
      SFVAR(ClearACKDelay),
      SFARRAY16(RawPCMVolumeCache, 2),
      SFARRAY(_Port, sizeof(_Port)),

      SFVAR(Fader.Command),
      SFVAR(Fader.Volume),
      SFVAR(Fader.CycleCounter),
      SFVAR(Fader.CountValue),
      SFVAR_BOOL(Fader.Clocked),

      SFARRAY(&SubChannelFIFO.data[0], SubChannelFIFO.size),
      SFVAR(SubChannelFIFO.read_pos),
      SFVAR(SubChannelFIFO.write_pos),
      SFVAR(SubChannelFIFO.in_count),

      SFEND
   };

   int ret = MDFNSS_StateAction(sm, load, data_only, StateRegs, "PECD", false);
   ret &= PCECD_Drive_StateAction(sm, load, data_only, "CDRM");
   ret &= ADPCM_StateAction(sm, load, data_only);

   if(load)
   {
      Fader_SyncWhich();
      //PCECD_Drive_SetDB(_Port[1]);
      PCECD_Drive_SetACK(ACKStatus);
      PCECD_Drive_SetRST(_Port[4] & 0x2);

      SubChannelFIFO.read_pos %= SubChannelFIFO.size;
      SubChannelFIFO.write_pos %= SubChannelFIFO.size;
   }
   return(ret);
}
